iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
AI Engineering

30 天打造 AI 後端:從 LLM、RAG 到 AI Agent系列 第 14

[Day 14] 我前六天在優化錯的東西:top-1 命中 10/15,答案卻對了 15/15

  • 分享至 

  • xImage
  •  

同一組 15 題,Day 11 的 Chroma 量到 top-1 命中 10/15。今天把它接上 LLM,
答案正確 15/15

兩個數字不只不一樣,方向還是反的——我六天來當成成績單的那個指標,
天花板比實際表現還低 5 題。

(基準線取 Chroma 那一套;Day 12、13 換庫量到的 top-1 各有出入,
今天刻意繞回 Day 11。)

本篇你會帶走

  • top-1 與答案正確率會脫鉤:top-1 凍在 10/15,答對率從 2 → 10 → 14 → 15,
    而落差完全不在生成端
  • recall@k 幾乎就是答對率的上限:k=3 花 k=5 的 64% token 就拿到 14/15
  • 一把可以抄走的判定尺(關鍵事實比對,0 元、可重現),以及它今天誤判了哪一題

一、為什麼今天做這件事

RAG=先從自己的資料撈出相關段落,再交給 LLM 寫答案(Day 8 有完整說明)。

前六天我只做了上半截:切塊(Day 9)、建索引(Day 10)、Chroma(Day 11),
Day 12、13 又換了,比較過一輪。成績單一直是同一個指標:top-1 命中率,Chroma 的數字是 10/15。

但這個指標藏著一個假設:看檢索結果的是人。人只想看第一名,所以第一名
要對。今天讀者換成 LLM——它一次讀完 k 段。假設一換,尺就得換。

今天刻意繞回 Chroma 而不用最後一天那套:要量生成端,檢索側就必須是已經有
基準線的那一套,落差才能乾淨歸因。語料與題目一字未改,檢索側參數全部凍結。

二、做法:把 top-k 接上生成

四步,全部在 pipeline.answer() 裡:

問題「特休沒休完會怎樣?」
  ├─(1) 算問題的向量            ← Day 8 做過
  ├─(2) Chroma 取 top-k 條文     ← Day 11 做過
  ├─(3) top-k 編號後組進 prompt  ← 今天新增
  └─(4) 丟給 LLM 產生答案        ← 今天新增

prompt 刻意只放兩條約束:

SYSTEM = (
    "你是公司人資的規章問答助理。"
    "請只依據使用者提供的「規章條文」回答,不要引用條文以外的法律知識或常識。"
    "答案用繁體中文,控制在三句話以內,直接講結論與關鍵數字。"
)

第一句是 RAG 的命脈。少了它,模型會拿預訓練的勞基法常識作答——語料換掉
答案也不會變,那你架的不是 RAG,是一台會背法條的機器。(所以有一組
「不給任何條文」的對照組專門量這件事。)

第二句是為了錢:輸出 token 比輸入貴 4 倍。實測有效——對照組平均 63.9 字,
給了條文的三組是 39~43 字。

context 每段都編號([1] 第 23 條(第五章 請假管理) 這種格式)。不是為了
好看:今天要分析「答案排第幾名時模型還讀不讀得到」,明天要問它「你用了
第幾段」,都需要這個位置標記。

三、實驗設計:換一把尺

四組對照:無 context、k=1、k=3、k=5。三個指標一起看:

指標 定義 誰的成績
top-1 命中率 預期條文排第一名 Day 8~13 的舊尺
recall@k 預期條文出現在 k 個之內 檢索真正交給生成的
答案正確率 答案含有全部關鍵事實 今天的新尺

答案怎麼判對錯:我沒用 LLM 當裁判(裁判自己會錯、會偏心,重跑要再付錢,
數字不可重現),改用關鍵事實比對——每題預先寫下答案必須出現的事實,
全出現才算對。

{"id": 1, "question": "病假連續請幾天以上需要附診斷證明?",
 "expected": "第 24 條", "facts": ["三日", "診斷證明"], "forbid": []}

比對前兩邊都正規化(中文數字轉阿拉伯數字、「天」=「日」、去標點),
所以模型答「連續請病假 3 天以上須附診斷證明書」也算對。

這把尺的偏誤先自首三個:

  1. 只看「有沒有講到」,不看「有沒有講錯」。forbid 補這個洞,但只補得到
    我想得到的錯法。
  2. facts 是我挑的,挑寬鬆分數就虛高。所以一律照條文原文取,不照模型
    答案回頭湊。
  3. 不看語氣。模型答「條文未提及」會算錯,但那其實是好行為——這個矛盾是
    明天的主題。

先記著第 1、2 點。今天最漂亮的那個滿分,就是被它們一起放過去的。

四、結果

組別 top-1 recall@k 答案正確 答案字數 context 字數 tokens(輸入/輸出)
無 context 2/15 63.9 0.0 952/856
k=1 10/15 10/15 10/15 39.3 137.5 3649/554
k=3 10/15 14/15 14/15 42.0 412.1 7952/609
k=5 10/15 14/15 15/15 42.8 696.0 12403/613

top-1 是同一次檢索算出來的,三組必然相同。該看的是中間兩欄走勢完全一致,
而最左邊那欄一動也沒動。

對照組:檢索到底貢獻了多少

不給條文,15 題只對 2 題——病假三日附診斷證明、加班上限 46 小時,
都是規章剛好與勞基法一致的題。它答對的不是我的規章,是預訓練學過的
法律常識。

真正的證據在錯的 13 題:它不是拒答,是掛著「根據公司規章」講錯數字

題目 對照組的答案 規章實際規定
婚假幾天? 「根據公司規章……五天 八日(第 27 條)
事假一年最多? 「根據公司規章……七天 十四日(第 25 條)
幾天以上要職代? 「請假超過三天以上」 一日以上(第 30 條)
特休沒休完? 自動作廢,無法累積」 遞延至次年 3/31,屆期折算工資(第 23 條)
離職提前多久講? 「至少兩週 十日/二十日/三十日(第 54 條)

「根據公司規章」這五個字最危險。它沒讀過我的規章,卻用這個句式把常識
包裝成內部規定——要是我只隨手問幾題、不逐題比對 facts,這些答案全會
通過我的目視檢查。

從 2/15 到 15/15,中間那 13 題全是檢索換來的。

現象一:落差不在生成端,一題都不在

組別 撈到且答對 撈到但答錯 沒撈到卻答對 沒撈到且答錯
k=1 10 0 0 5
k=3 14 0 0 1
k=5 14 0 1 0

「檢索做對了、生成搞砸了」那一欄,三組都是 0。

我原本預期這裡會有題目——條文就在 prompt 裡,把兩條規定混在一起或抓錯
數字都很常見;今天一題都沒發生。只要條文進了 prompt,gpt-4o-mini 就把
數字照抄出來。

所以在這個規模下,答對率的天花板是 recall@k,不是 top-1。兩者在 k=3
差了 4 題,我前六天優化的是低的那個。

邊界要說清楚:語料 59 條、每題單一條文可答、答案都是數字與制度名詞。
多條文綜合題(「加班 3 小時領多少錢」要第 19 條+第 33 條)今天量不到。

現象二:排到第三名的條文,模型讀得到

預期條文排名 k=3 題數/答對 k=5 題數/答對
第 1 名 10/10 10/10
第 2 名 2/2 2/2
第 3 名 2/2 2/2
未進 top-k 1/0 1/1

沒有「餵進去卻讀不到」的現象。兩個直接後果:該追 recall@k,不是 top-1
(排序好看是給人的需求,模型只在乎有沒有在裡面);先別急著上 reranker
——重排序解的是「排後面讀不到」,而這個問題今天不存在,k 拉大就解決了。

第 7 題「特休沒休完會怎樣?」:k=1 沒撈到第 23 條,模型老實回「規章條文
中未提及」,判定算錯;k=3 之後它排到第 3 名,答案就變成「得遞延至次年
三月三十一日止,屆期按日折算工資發給」,關鍵事實全中。同一個模型、
同一個 prompt,差別只在那段條文有沒有進 context。

現象三:那個 15/15 是假的

k=5 唯一從「未進 top-k」逆轉的是第 9 題「沒來上班也沒跟任何人講,會有
什麼後果?」,預期第 32 條(曠職),facts 只要求出現「曠職」。

k=5 撈回第 47、12、49、13、16 條——第 32 條依然沒進 prompt。模型答:

未經請假或通知擅自缺勤,將視為曠職,依第八章獎懲規定處理。

判定 ✅。但「曠職」在整段 context 只出現一次,在第 16 條(天然災害停班):

……員工因災害實際受阻無法出勤者,得依第五章規定請假,
公司不得視為曠職

語意相反。模型從「不得視為曠職」生成「將視為曠職」,結論恰好正確;
而第 32 條真正的規定(曠職不發工資、連續三日或一個月累計六日得終止契約)
從頭到尾沒進 prompt。

這題把前面自首的偏誤 1 和 2 同時演了一次:尺只查關鍵詞,而我把這題的
facts 挑成單獨一個詞。
總表最誠實的寫法是「k=5:14/15 + 1 題蒙對」。

我留著不修,因為修掉就看不到這件事。要補有兩個方向:facts 加上
「不發工資」把門檻拉高,或像明天那樣改判引用的條號對不對——第 9 題在
k=1/k=3 都引了第 47 條,那是憑空安上去的依據,比答錯數字更難發現。

五、決策速查表

情境 建議 代價
評估自己的 RAG 別只量檢索命中率,要量答案正確率——今天差 5 題 得先定義「答案對不對」,沒有免費解
挑指標 追 recall@k,別追 top-1 前提是生成端讀得到後面的段落,要自己驗
定判定標準 關鍵事實比對優先於 LLM 裁判,但 facts 別只寫一個詞 只適用答案含明確事實的語料
挑 k 值 k=3 是甜蜜點:14/15,輸入 token 只有 k=5 的 64% 輸入 token 隨 k 線性成長(k=1→5 是 3.4 倍)
換真實語料前 條文原文會整段送進生成 API 這是 Day 8~13 沒有的第二條外流路徑

六、這次花了多少錢

  • Embedding:0 tokens(快取命中 59 塊)≈ 0 元
  • 生成:輸入 24,956、輸出 2,632 tokens ≈ 新台幣 0.165 元(60 次生成:
    4 組 × 15 題)

順便注意對照組輸出 856 tokens 比 k=5 的 613 還多:沒有條文可講的時候,
它話最多。chat_cache.json 以「模型+完整 messages」雜湊為 key,
temperature=0 下重跑走快取、拿到跟本文一樣的數字;刪掉兩個快取檔
重跑可重現完整成本。

小結

檢索的成績單和 RAG 的成績單不是同一張。前六天我每天在把 top-1 從 9 推到
10,而真正決定答案對不對的是 recall@k——同一次檢索、同一份數據,我看的
是低的那一欄。

還有一個更難受的:今天生成端一題都沒搞砸,意思是這 15 題的難度根本不在
生成。全對的實驗代表考題太簡單,所以明天要加難度。

你的 RAG 上線前,量的是檢索命中率,還是答案正確率?如果是前者,你怎麼
知道模型真的讀懂了撈回來的東西?

明天 Day 15:今天有一類題目讓判定尺當機——模型說「條文未提及」的時候
(第 7 題 k=1 就是,答得老實卻算它錯)。反過來更麻煩:規章真的沒寫的
問題,它照樣掰得出答案,就像今天對照組那 13 題。明天加五題語料裡根本
沒有答案的陷阱題,量幻覺率、該拒答時拒答、過度拒答與引用正確率,
並要求它標出條號,看這會不會反而讓正常題答得更差。

完整程式碼

GitHub:https://github.com/wp900622/TrustRAG (day14_rag/,clone 後
python build_db.py && python run_experiment.py 即可重現)

  • OpenAI Chat Completions 文件:https://platform.openai.com/docs/api-reference/chat

上一篇
[Day 13] 向量資料庫實際操作 — Qdrant 與 pgvector
下一篇
[DAY 15]RAG 到底要評估什麼?我發現 Retrieval 命中率根本不等於答案正確率
系列文
30 天打造 AI 後端:從 LLM、RAG 到 AI Agent26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言